from typing import Optional

import requests
import aiohttp
from yarl import URL
from pydantic import ValidationError

from . import config
from .request import ApiRequest
from .signers import AKSKSigner
from .serializers import JSONSerializer
from . import inputs
from . import outputs


class TxAPIError(Exception):
    def __init__(
            self,
            status_code: int,
            message: str,
            error_code: Optional[int] = None
    ):
        """
        :param status_code: api响应HTTP状态码
        :param message: 腾讯会议api返回的错误描述
        :param error_code: 腾讯会议api返回的错误码
        """
        self.status_code = status_code
        self.error_code = error_code
        self.message = message

    def __str__(self):
        return self.detail_message()

    def detail_message(self) -> str:
        if self.status_code > 0:
            return f'Http code {self.status_code}，错误码 {self.error_code}，{self.message}'
        else:
            return f'错误码 {self.error_code}，{self.message}'


class TxMeetingClient:
    def __init__(
            self,
            app_id: str,
            sdk_id: str,
            secret_id: str,
            secret_key: str,
            operator_id: str,
            operator_id_type: int = 1
    ) -> None:
        """
        operator_id: 默认操作者ID，需要一个有管理员权限的用户，腾讯会议接口参数需要
        operator_id_type: 默认操作者 ID 类型，1：userid
        """
        self.__config: config.Config = config.Config(
            protocol=config.DEFAULT_PROTOCOL,
            host=config.OPEN_API_DOMAIN,
            app_id=app_id,
            sdk_id=sdk_id,
            secret_id=secret_id,
            secret_key=secret_key
        )
        self.operator_id = operator_id
        self.operator_id_type = operator_id_type

    def _do_request(self, req: ApiRequest) -> requests.Response:
        """
        :raises: Exception
        """
        # 序列化请求体
        data: bytes = b''
        if req.body is not None:
            if isinstance(req.body, (str, bytes)):
                data = req.body.encode("utf-8")
            else:
                data = JSONSerializer.serialize(req.body)

        # 构建带域名的url
        api_url = req.build_absolute_uri(f"{self.__config.protocol}://{self.__config.host}")

        # 构造 requests http 请求
        # bytes类型的数据可以使用参数data传递，dict类型的数据需要通过参数json传递，这由requests底层body处理逻辑决定的
        http_preq = requests.Request(method=req.method, url=api_url, params=req.query_params, data=data).prepare()

        # 设置请求头
        for k, v in req.headers().items():
            http_preq.headers[k] = v

        # 增加鉴权头
        AKSKSigner(conf=self.__config).add_auth_headers(http_req=http_preq)

        with requests.Session() as session:
            return session.send(http_preq, timeout=(8, 60))    # connect=8, read=60

    def list_corp_meetings(
            self, params: inputs.ListMeetingsInput
    ) -> outputs.ListMeetingsOutput:
        """
        通过会议号查询企业会议信息
        """
        query_params = params.model_dump(exclude_none=True)
        if not query_params.get('operator_id'):
            query_params['operator_id'] = self.operator_id
            query_params['operator_id_type'] = self.operator_id_type

        req: ApiRequest = ApiRequest(
            method='GET',
            api_path='/v1/corp/meetings',
            query_params=query_params
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求腾讯会议查询API错误，{str(exc)}')
            return outputs.ListMeetingsOutput(ok=False, error=error, data=None, raw_response=None)

        if response.status_code == 200:
            try:
                data = outputs.ListMeetingsData.model_validate_json(response.content)
            except Exception as e:
                error = TxAPIError(status_code=200, message=f'解析腾讯会议查询API响应数据错误，{str(e)}')
                return outputs.ListMeetingsOutput(ok=False, error=error, data=None, raw_response=response)

            return outputs.ListMeetingsOutput(ok=True, error=None, data=data, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.ListMeetingsOutput(ok=False, error=error, data=None, raw_response=response)

    def list_reserve_meeting_room_users(
            self, params: inputs.ListReserveMeetingRoomUserInput
    ) -> outputs.ListReserveMeetingRoomUserOutput:
        """
        查询企业预留虚拟会议室成员列表

            当账户类型为企业版或教育版时，可查询企业预留虚拟会议室成员列表，支持根据用户 ID 或用户名搜索。

            * 错误码列表
            状态码 400:
                190004: 参数非法，请对照接口文档检查您的参数。
            状态码 500:
                20: 查询相关资源异常，请稍后重试。
                41: 网络错误，请稍后重试。
                403: 没有资源操作权限。
                190439: 企业状态异常，场景：
                        1：企业非商业版与企业版
                        2：企业信息已过期或已被删除
        """
        query_params = params.model_dump(exclude_none=True)
        if not query_params.get('operator_userid'):
            query_params['operator_userid'] = self.operator_id

        req: ApiRequest = ApiRequest(
            method='GET',
            api_path='/v1/corp-resource/reserve-virtual-meeting-room/authorized-users',
            query_params=query_params
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=200, message=f'请求企业预留虚拟会议室成员查询API错误，{str(exc)}')
            return outputs.ListReserveMeetingRoomUserOutput(ok=False, error=error, data=None, raw_response=None)

        if response.status_code == 200:
            try:
                data = outputs.ListReserveMeetingRoomUserData.model_validate_json(response.content)
            except Exception as e:
                error = TxAPIError(status_code=200, message=f'解析企业预留虚拟会议室成员查询API响应数据错误时，{str(e)}')
                return outputs.ListReserveMeetingRoomUserOutput(ok=False, error=error, data=None, raw_response=response)

            return outputs.ListReserveMeetingRoomUserOutput(ok=True, error=None, data=data, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.ListReserveMeetingRoomUserOutput(ok=False, error=error, data=None, raw_response=response)

    def post_reserve_meeting_room_users(
            self, params: inputs.PostReserveMeetingRoomUserInput
    ) -> outputs.NoContentOutput:
        """
        设置企业预留虚拟会议室名单

            当账户类型为企业版或教育版时，可为企业下具有发起会议权限的成员预留虚拟会议室。
            若企业成员未预留过虚拟会议室，则为成员预留虚拟会议室；
            若企业成员预留过虚拟会议室，则可调整用户虚拟会议室信息，覆盖已有的虚拟会议室信息。

            * 错误码列表
            状态码 400:
                190004: 参数非法，请对照接口文档检查您的参数。
            状态码 500:
                20: 查询相关资源异常，请稍后重试。
                403: 没有资源操作权限。
        """
        body = params.model_dump(exclude_none=True)
        if not body.get('operator_userid'):
            body['operator_userid'] = self.operator_id

        req: ApiRequest = ApiRequest(
            method='POST',
            api_path='/v1/corp-resource/reserve-virtual-meeting-room/authorized-users',
            body=body
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求设置企业预留虚拟会议室名单API错误，{str(exc)}')
            return outputs.NoContentOutput(ok=False, error=error, raw_response=None)

        if response.status_code == 200:
            return outputs.NoContentOutput(ok=True, error=None, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.NoContentOutput(ok=False, error=error, raw_response=response)

    def remove_reserve_meeting_room_users(
            self, params: inputs.RemoveReserveMeetingRoomUserInput
    ) -> outputs.NoContentOutput:
        """
        移除企业预留虚拟会议室名单

            当账户类型为企业版或教育版时，可批量移除企业下具有发起会议权限用户已预留的虚拟会议室

            * 错误码列表
            状态码 400:
                190004: 参数非法，请对照接口文档检查您的参数。
            状态码 500:
                20: 查询相关资源异常，请稍后重试。
                403: 没有资源操作权限。
        """
        body = params.model_dump(exclude_none=True)
        if not body.get('operator_userid'):
            body['operator_userid'] = self.operator_id

        req: ApiRequest = ApiRequest(
            method='POST',
            api_path='/v1/corp-resource/reserve-virtual-meeting-room/authorized-users/remove',
            body=body
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求移除企业预留虚拟会议室名单API错误，{str(exc)}')
            return outputs.NoContentOutput(ok=False, error=error, raw_response=None)

        if response.status_code == 200:
            return outputs.NoContentOutput(ok=True, error=None, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.NoContentOutput(ok=False, error=error, raw_response=response)

    def dismiss_meeting(
            self, params: inputs.DismissMeetingInput
    ) -> outputs.NoContentOutput:
        """
        结束一个进行中的会议。

            * 只有会议创建者、主持人、联席主持人可以结束会议，且该会议是一个有效的进行中会议。
            * 结束周期性会议需要传入主会议 ID。
            * 企业 secret 鉴权用户可结束任何该企业用户创建的进行中的会议，OAuth 2.0鉴权用户只能结束通过 OAuth 2.0鉴权创建的进行中的会议。
            * 当您想实时监测会议结束状况时，您可以通过订阅 会议结束 的事件，接收事件通知。
            * 此接口暂不支持 MRA 设备作为被操作者的情况。
            * 结束会议不会释放 Rooms，如需释放请调用 释放会议室（Rooms）接口。

            * 错误码列表
            状态码 400:
                190301: 请求重放错误，请求头里的 X-TC-Timestamp 和 X-TC-Nonce 每次请求时需重新生成，
                        不能使用和过去五分钟内相同的 timestamp 和 nonce。
                190457: 会议不存在,   请核对 meeting_id。（实测过）
                190004: 参数非法，请对照接口文档检查您的参数。（实测过，会议id格式无效不是整数型）
            状态码 500:
                41: 网络错误，请稍后重试。
                9042: 无权限进行该操作，建议检查操作者身份或资源归属。
                13001: 缓存设置会议信息失败。
                960000: 网络错误，请稍后重试。
                190308: 查询 tinyID 失败。
                190310: 每分钟总接口调用次数超过限制。
                9003: 会议信息不存在。
                190001: 存在未注册的用户。
                13651: 会议已取消。   (实测过)
                13000: 缓存查询会议信息失败。
                13652: 非强制解散会议时有成员在会议中。
                9108: 用户 sdkID 认证失败。
                190456: 查询用户不存在，请检查该用户是否注册。
        """
        body = params.model_dump(exclude_none=True)
        if not body.get('operator_id'):
            body['operator_id'] = self.operator_id
            body['operator_id_type'] = self.operator_id_type

        meeting_id = body.pop('meeting_id')
        req: ApiRequest = ApiRequest(
            method='POST',
            api_path=f'/v1/meetings/{meeting_id}/dismiss',
            body=body
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求结束一个进行中的会议API错误，{str(exc)}')
            return outputs.NoContentOutput(ok=False, error=error, raw_response=None)

        if response.status_code == 200:
            return outputs.NoContentOutput(ok=True, error=None, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.NoContentOutput(ok=False, error=error, raw_response=response)

    def get_meeting_detail(
            self, params: inputs.GetMeetingDetailInput
    ) -> outputs.GetMeetingDetailOutput:
        """
        通过会议 ID 查询会议详情。

            * 企业 secret 鉴权用户可查询到任何该用户创建的企业下的会议，OAuth2.0 鉴权用户只能查询到通过 OAuth2.0 鉴权创建的会议。
            * 支持企业管理员查询企业下的会议。
            * 当会议为周期性会议时，主持人密钥每场会议固定，但单场会议只能获取一次。支持查询周期性会议的主持人密钥。
            * 支持查询 MRA 当前所在会议信息。

            * 错误码列表
            状态码 400:
                500014: 账号无权限。
                190301: 请求重放错误，请求头里的 X-TC-Timestamp 和 X-TC-Nonce 每次请求时需重新生成，不能使用和过去五分钟内相同的 timestamp 和 nonce。
                190300: 请求头里的时间戳参数 X-TC-Timestamp 取值与服务器当前时间差距超过5分钟，视为非法请求。
                190004: 参数非法，请对照接口文档检查您的参数。
            状态码 500:
                41: 网络错误，请稍后重试。
                13000: 缓存查询会议信息失败。
                500001: 会议号无效。
                20000: 请检查参数类型
                202007: 成员不在会中。
                9042: 无权限进行该操作，建议检查操作者身份或资源归属。
                13704: 查询会议列表失败。
                13703: 查询会议失败。
                13702: 查询会议详情失败。
                9108: 用户 sdkID 认证失败。
                960000: 网络错误，请稍后重试。
                190001: 存在未注册的用户。
                190310: 每分钟总接口调用次数超过限制。
                9003: 会议信息不存在。（会议id不存在时，实测验证过）
        """
        query_params = params.model_dump(exclude_none=True)
        if not query_params.get('operator_id'):
            query_params['operator_id'] = self.operator_id
            query_params['operator_id_type'] = self.operator_id_type

        meeting_id = query_params.pop('meeting_id')
        req: ApiRequest = ApiRequest(
            method='GET',
            api_path=f'/v1/meetings/{meeting_id}',
            query_params=query_params
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求查询会议详情API错误，{str(exc)}')
            return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=None)

        if response.status_code == 200:
            try:
                data = outputs.GetMeetingDetailData.model_validate_json(response.content)
            except Exception as e:
                error = TxAPIError(status_code=200, message=f'解析会议详情查询API响应数据错误时，{str(e)}')
                return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=response)

            return outputs.GetMeetingDetailOutput(ok=True, error=None, data=data, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=response)

    def get_meeting_detail_by_code(
            self, params: inputs.GetMeetingDetailByCodeInput
    ) -> outputs.GetMeetingDetailOutput:
        """
        通过会议号 查询会议详情。

            * 企业 secret 鉴权用户可查询到任何该用户创建的企业下的会议，OAuth2.0 鉴权用户只能查询到通过 OAuth2.0 鉴权创建的会议。
            * 支持企业管理员查询企业下的会议。
            * 当会议为周期性会议时，主持人密钥每场会议固定，但单场会议只能获取一次。支持查询周期性会议的主持人密钥。
            * 支持查询 MRA 当前所在会议信息。
            * 若会议号被回收则无法通过 Code 查询，您可以通过会议 ID 查询到该会议。

            * 错误码列表
            状态码 400:
                500014: 账号无权限。
                190301: 请求重放错误，请求头里的 X-TC-Timestamp 和 X-TC-Nonce 每次请求时需重新生成，不能使用和过去五分钟内相同的 timestamp 和 nonce。
                190300: 请求头里的时间戳参数 X-TC-Timestamp 取值与服务器当前时间差距超过5分钟，视为非法请求。
                190004: 参数非法，请对照接口文档检查您的参数。
            状态码 500:
                41: 网络错误，请稍后重试。
                10001: 参数异常
                13000: 缓存查询会议信息失败。
                500001: 会议号无效。
                20000: 请检查参数类型
                202007: 成员不在会中。
                9042: 无权限进行该操作，建议检查操作者身份或资源归属。
                13704: 查询会议列表失败。
                13703: 查询会议失败。
                13702: 查询会议详情失败。
                9108: 用户 sdkID 认证失败。
                960000: 网络错误，请稍后重试。
                190001: 存在未注册的用户。
                190310: 每分钟总接口调用次数超过限制。
                9003: 会议信息不存在。（会议id不存在时，实测验证过）
        """
        query_params = params.model_dump(exclude_none=True)
        if not query_params.get('operator_id'):
            query_params['operator_id'] = self.operator_id
            query_params['operator_id_type'] = self.operator_id_type

        req: ApiRequest = ApiRequest(
            method='GET',
            api_path=f'/v1/meetings',
            query_params=query_params
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'通过会议号查询会议详情API错误，{str(exc)}')
            return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=None)

        if response.status_code == 200:
            try:
                data = outputs.GetMeetingDetailData.model_validate_json(response.content)
            except Exception as e:
                error = TxAPIError(status_code=200, message=f'解析会议详情查询API响应数据错误时，{str(e)}')
                return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=response)

            return outputs.GetMeetingDetailOutput(ok=True, error=None, data=data, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=response)

    @staticmethod
    def _build_error_form_response(response: requests.Response) -> Optional[TxAPIError]:
        if response.status_code < 300:
            return None

        error_message = response.text
        error_code = -1
        try:
            data = response.json()
            error_info = data.get("error_info")
            if error_info:
                error_code = error_info.get("error_code")
                error_message = error_info.get("message")
        except Exception:
            pass

        return TxAPIError(status_code=response.status_code, error_code=error_code, message=error_message)

    @staticmethod
    def get_msg_pydantic_error(err: ValidationError) -> str:
        try:
            errs = err.errors()
            if errs:
                msg = errs[0]['msg']
            else:
                msg = str(err)
        except Exception:
            msg = str(err)

        return msg

    @staticmethod
    async def _async_build_error_form_response(response: aiohttp.ClientResponse) -> Optional[TxAPIError]:
        if response.status < 300:
            return None

        error_message = await response.text()
        error_code = -1
        try:
            data = await response.json()
            error_info = data.get("error_info")
            if error_info:
                error_code = error_info.get("error_code")
                error_message = error_info.get("message")
        except Exception:
            pass

        return TxAPIError(status_code=response.status, error_code=error_code, message=error_message)

    async def _async_do_request(self, req: ApiRequest) -> aiohttp.ClientResponse:
        """
        :raises: Exception
        """
        # 序列化请求体
        data: bytes = b''
        if req.body is not None:
            if isinstance(req.body, (str, bytes)):
                data = req.body.encode("utf-8")
            else:
                data = JSONSerializer.serialize(req.body)

        # 构建带域名的url
        api_url_str = req.build_absolute_uri(base_url=f"{self.__config.protocol}://{self.__config.host}", with_query=True)
        api_url = URL(api_url_str)
        path_url = api_url.raw_path_qs
        # 增加鉴权头
        headers = AKSKSigner(conf=self.__config).build_auth_headers(
            method=req.method, path_url=path_url, body=data)
        if req.headers:
            headers.update(req.headers())

        async with aiohttp.ClientSession() as client:
            r = await client.request(
                method=req.method,
                url=api_url,
                data=data,
                params=None,
                headers=headers,
                timeout=aiohttp.ClientTimeout(sock_connect=8, total=30)
            )
            await r.read()
            return r

    async def async_get_meeting_detail(
            self, params: inputs.GetMeetingDetailInput
    ) -> outputs.GetMeetingDetailOutput:
        """
        通过会议 ID 查询会议详情。

            * 企业 secret 鉴权用户可查询到任何该用户创建的企业下的会议，OAuth2.0 鉴权用户只能查询到通过 OAuth2.0 鉴权创建的会议。
            * 支持企业管理员查询企业下的会议。
            * 当会议为周期性会议时，主持人密钥每场会议固定，但单场会议只能获取一次。支持查询周期性会议的主持人密钥。
            * 支持查询 MRA 当前所在会议信息。
        """
        query_params = params.model_dump(exclude_none=True)
        if not query_params.get('operator_id'):
            query_params['operator_id'] = self.operator_id
            query_params['operator_id_type'] = self.operator_id_type

        meeting_id = query_params.pop('meeting_id')
        req: ApiRequest = ApiRequest(
            method='GET',
            api_path=f'/v1/meetings/{meeting_id}',
            query_params=query_params
        )
        try:
            response = await self._async_do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求查询会议详情API错误，{str(exc)}')
            return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=None)

        if response.status == 200:
            try:
                data = outputs.GetMeetingDetailData.model_validate_json(await response.read())
            except Exception as e:
                error = TxAPIError(status_code=200, message=f'解析会议详情查询API响应数据错误时，{str(e)}')
                return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=response)

            return outputs.GetMeetingDetailOutput(ok=True, error=None, data=data, raw_response=response)

        error = await self._async_build_error_form_response(response)
        return outputs.GetMeetingDetailOutput(ok=False, error=error, data=None, raw_response=response)

    def get_meeting_participants(
            self, params: inputs.GetMeetingParticipantsInput
    ) -> outputs.GetMeetingParticipantsOutput:
        """
        获取参会成员明细

            * 支持查询网络研讨会参会成员列表。
            * 如果会议还未开始，调用此接口查询会返回空列表。
            * 企业 secret 鉴权用户（会议创建者）可获取任何该企业用户创建的有效会议中的参会成员，企业 secret 鉴权用户（企业超级管理员）可获取任何该企业下创建的有效会议中的参会成员，OAuth 2.0鉴权用户（会议创建者）只能获取用户通过 OAuth 2.0鉴权创建的有效会议中的参会成员。
            * 当您需要实时监测参会成员入会状态或离会状态时，您可以通过订阅 用户入会  和 用户离会 的事件，接收事件通知。

            * 错误码列表
            状态码 400:
                9003: 会议信息不存在。
                190301: 请求重放错误，请求头里的 X-TC-Timestamp 和 X-TC-Nonce 每次请求时需重新生成，不能使用和过去五分钟内相同的 timestamp 和 nonce。
                200001: API 请求头缺失必填字段。
                500014：账号无权限。
                190004: 参数非法，请对照接口文档检查您的参数。
            状态码 403：
                9042：无权限操作。
            状态码 500:
                41: 网络错误，请稍后重试。
                190310: 每分钟总接口调用次数超过限制。
                190001: 存在未注册的用户。
                10000：用户信息 API 调用通用错误返回。
                960000：网络错误，请稍后重试。
        """
        query_params = params.model_dump(exclude_none=True)
        # if not query_params.get('operator_id'):
        #     query_params['operator_id'] = self.operator_id
        #     query_params['operator_id_type'] = self.operator_id_type

        meeting_id = query_params.pop('meeting_id')
        req: ApiRequest = ApiRequest(
            method='GET',
            api_path=f'/v1/meetings/{meeting_id}/participants',
            query_params=query_params
        )
        try:
            response = self._do_request(req)
        except Exception as exc:
            error = TxAPIError(status_code=0, message=f'请求查询参会成员明细API错误，{str(exc)}')
            return outputs.GetMeetingParticipantsOutput(ok=False, error=error, data=None, raw_response=None)

        if response.status_code == 200:
            try:
                data = outputs.GetMeetingParticipantsData.model_validate_json(response.content)
            except Exception as e:
                error = TxAPIError(status_code=200, message=f'解析参会成员明细查询API响应数据错误时，{str(e)}')
                return outputs.GetMeetingParticipantsOutput(ok=False, error=error, data=None, raw_response=response)

            return outputs.GetMeetingParticipantsOutput(ok=True, error=None, data=data, raw_response=response)

        error = self._build_error_form_response(response)
        return outputs.GetMeetingParticipantsOutput(ok=False, error=error, data=None, raw_response=response)
